﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;

namespace Oxage.CNC.Models
{
	public class GCodeBlock : IMachineCodeBlock
	{
		#region Constructor
		public GCodeBlock()
		{
			this.inlines = new List<IMachineCodeInline>();
		}

		public GCodeBlock(string block)
		{
			this.inlines = new List<IMachineCodeInline>();
			Parse(block);
		}
		#endregion

		#region Public properties
		public int LineNumber
		{
			get;
			set;
		}
		#endregion

		public enum GCodePlane
		{
			XY = 17,
			XZ = 18,
			YZ = 19,
		}

		public enum GCodeMode
		{
			/// <summary>
			/// G90..absolute coordinates
			/// </summary>
			Absolute = 90,
			/// <summary>
			/// G91..incremental coordinates
			/// </summary>
			Relative = 91
		}

		public enum GCodeUnits
		{
			/// <summary>
			/// G20..inch units
			/// </summary>
			Inch = 20,
			/// <summary>
			/// G21..mm units
			/// </summary>
			Millimeters = 21
		}

		public enum GCodeMovement
		{
			RapidMove = 0,
			LinearInterpolation = 1,
			ArcClockwise = 2,
			ArcCounterClockwise = 3,
		}

		public class GCodeSegmentCollection : List<GCodeInline>
		{
			public GCodeSegmentCollection(GCodeMode mode, Dictionary<char, double> previous)
			{
				this.mode = mode;
				this.previous = previous;
			}

			private GCodeMode mode;
			private Dictionary<char, double> previous;

			public Dictionary<char, double> Previous
			{
				get
				{
					return this.previous;
				}
			}

			public double GetValue(char code)
			{
				var lower = code.ToString().ToLower()[0];
				var upper = code.ToString().ToUpper()[0];
				var segment = this.FirstOrDefault(x => x.Code == upper || x.Code == lower);
				double? raw = (segment != null && segment.Value != null ? segment.Value : null);

				if (raw != null)
				{
					return raw.Value;
				}

				switch (mode)
				{
					case GCodeMode.Absolute:
						return previous.ContainsKey(upper) ? previous[upper] : 0;

					case GCodeMode.Relative:
						return 0;
				}

				throw new Exception();
			}
		}

		//Current state
		private GCodeMode mode = GCodeMode.Absolute;
		private GCodePlane plane = GCodePlane.XY;
		private GCodeUnits units = GCodeUnits.Millimeters;
		private Dictionary<char, double> previous = new Dictionary<char, double>
		{
			 { 'X', 0 }, { 'Y', 0 }, { 'Z', 0 },
		};

		#region Public methods
		public void Parse(string line)
		{
			//TODO: Find block comment

			//Matches a line of G code
			var match = Regex.Match(line, @"^\s*(?<code>\w[\d\-\+\.]+\s*)*\s*(?<comment>;.*)?$", RegexOptions.IgnoreCase);
			if (match.Success)
			{
				//foreach (Capture capture in match.Groups["code"].Captures)
				//{
				//  //Parse and add code (e.g. "G01", "X-.10", "Z+10")
				//  string code = capture.Value.Trim();
				//  this.Inlines.Add(new GCodeInline(code));
				//}

				var segments = new GCodeSegmentCollection(mode, previous);
				foreach (Capture capture in match.Groups["code"].Captures)
				{
					//Parse and add code (e.g. "G01", "X-.10", "Z+10")
					string code = capture.Value.Trim();
					segments.Add(new GCodeInline(code));
				}

				//TODO: Collect G, X, Y, Z... and create a specific inline (ArcInline, MoveInline)
				var segment = segments.FirstOrDefault(x => x.Code == 'G');
				if (segment != null)
				{
					double x = segments.GetValue('X');
					double y = segments.GetValue('Y');
					double z = segments.GetValue('Z');

					double px = segments.Previous['X'];
					double py = segments.Previous['Y'];
					double pz = segments.Previous['Z'];

					Point startPoint = new Point(px, py);
					Point centerPoint = new Point();
					Point endPoint = new Point();

					int value = (segment.Value != null ? (int)segment.Value : -1);
					switch (value)
					{
						case 0:
						case 1:
							var inline = new GCodeMoveInline();
							inline.Code = 'G';
							inline.Value = value;
							inline.EndPoint = new Point(x, y);
							this.Inlines.Add(inline);
							segments.Previous['X'] = x;
							segments.Previous['Y'] = y;
							segments.Previous['Z'] = z;
							break;

						case 2:
						case 3:
							var arc = new GCodeArcInline();
							arc.Code = 'G';
							arc.Value = value;
							startPoint = new Point(px, py);
							switch (plane)
							{
								case GCodePlane.XY:
									centerPoint = new Point(segments.GetValue('I'), segments.GetValue('J'));
									endPoint = new Point(segments.GetValue('X'), segments.GetValue('Y'));
									break;

								case GCodePlane.XZ:
									centerPoint = new Point(segments.GetValue('I'), segments.GetValue('K'));
									endPoint = new Point(segments.GetValue('X'), segments.GetValue('Z'));
									break;

								case GCodePlane.YZ:
									centerPoint = new Point(segments.GetValue('J'), segments.GetValue('K'));
									endPoint = new Point(segments.GetValue('Y'), segments.GetValue('Z'));
									break;

								default:
									throw new NotSupportedException();
							}
							bool clockwise = (value == 2); //G2..CW, G3..CCW
							arc.Calculate(startPoint, centerPoint, endPoint, clockwise);
							this.Inlines.Add(arc);
							segments.Previous['X'] = x;
							segments.Previous['Y'] = y;
							segments.Previous['Z'] = z;
							break;

						//Custom: bezier
						case 500:
							var bezier = new GCodeBezierInline();
							bezier.Code = 'G';
							bezier.Value = value;
							startPoint = new Point(px, py);
							endPoint = new Point(segments.GetValue('X'), segments.GetValue('Y')); //Z
							Point controlPoint1 = new Point(segments.GetValue('I'), segments.GetValue('J')); //K
							Point controlPoint2 = new Point(segments.GetValue('U'), segments.GetValue('V')); //W
							bezier.StartPoint = startPoint;
							bezier.ControlPoint1 = controlPoint1;
							bezier.ControlPoint2 = controlPoint2;
							bezier.EndPoint = endPoint;
							this.Inlines.Add(bezier);
							segments.Previous['X'] = x;
							segments.Previous['Y'] = y;
							segments.Previous['Z'] = z;
							break;
					}
				}
				else
				{
					//TODO: Handle other codes here (e.g. M code)
				}

				if (match.Groups["comment"] != null)
				{
					string comment = match.Groups["comment"].Value;
					this.Inlines.Add(new GCodeCommentInline(comment));
				}
			}
		}

		public override string ToString()
		{
			if (this.Inlines == null)
			{
				return "";
			}

			var builder = new StringBuilder();
			foreach (var inline in this.Inlines)
			{
				if (builder.Length > 0)
				{
					//White space between inlines
					builder.Append(" ");
				}
				builder.Append(inline.ToString());
			}
			return builder.ToString().Trim();
		}
		#endregion

		#region IMachineCodeBlock Members
		private List<IMachineCodeInline> inlines;

		public List<IMachineCodeInline> Inlines
		{
			get
			{
				return this.inlines;
			}
		}

		public void Add(IMachineCodeInline inline)
		{
			this.Inlines.Add(inline);
		}
		#endregion

		#region IEnumerable<IMachineCodeInline> Members
		public IEnumerator<IMachineCodeInline> GetEnumerator()
		{
			return this.inlines.GetEnumerator();
		}
		#endregion

		#region IEnumerable Members
		IEnumerator IEnumerable.GetEnumerator()
		{
			return this.inlines.GetEnumerator();
		}
		#endregion
	}
}
